package httpclient

import (
	"container/list"
	"context"
	"errors"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

type Options struct {
	Context   context.Context
	Client    *http.Client
	Hosts     *list.List
	CheckPath string //健康检查的请求路径
}

type Client struct {
	*Options
	pendingHostChan chan pendingHost
	mu              *sync.RWMutex
}

type pendingHost struct {
	host      string
	replyChan chan<- bool
}

func NewClient(options Options) (*Client, error) {
	if options.Hosts == nil || options.Hosts.Len() == 0 {
		return nil, errors.New("bad hosts")
	}
	if options.Context == nil {
		options.Context = context.Background()
	}
	if options.Client == nil {
		options.Client = http.DefaultClient
	}
	client := &Client{
		Options:         &options,
		pendingHostChan: make(chan pendingHost, 10),
		mu:              &sync.RWMutex{},
	}
	go client.checkPendingHosts()
	return client, nil
}

func (c *Client) checkPendingHosts() {
	// Create a new list and put some numbers in it.
	l := list.New()
	for {
		select {
		case <-c.Context.Done():
			return
		case pending := <-c.pendingHostChan:
			// println("pending:", pending.host)
			l.PushBack(pending.host)
			pending.replyChan <- true
		case <-time.After(5 * time.Second):
			todos := []*list.Element{}
			// Iterate through list and print its contents.
			for e := l.Front(); e != nil; e = e.Next() {
				host := e.Value.(string)
				res, err := http.Get(host + c.CheckPath)
				if err != nil {
					continue
				}
				res.Body.Close()
				todos = append(todos, e)
			}
			if len(todos) == 0 {
				continue
			}
			for _, e := range todos {
				l.Remove(e)
				host := e.Value.(string)
				c.mu.Lock()
				c.Hosts.PushBack(host)
				c.mu.Unlock()
			}
		}
	}
}

func (c *Client) getHost() *list.Element {
	len := c.Hosts.Len()
	if len == 0 {
		return nil
	}
	c.mu.RLock()
	defer c.mu.RUnlock()
	n := rand.Intn(len)
	e := c.Hosts.Front()
	for i := 0; i < n; i++ {
		e = e.Next()
	}
	return e
}

func (c *Client) moveToPendingHosts(e *list.Element) {
	reply := make(chan bool)
	c.pendingHostChan <- pendingHost{
		host:      e.Value.(string),
		replyChan: reply,
	}
	select {
	case <-reply:
		c.mu.Lock()
		c.Hosts.Remove(e)
		c.mu.Unlock()
	case <-time.After(100 * time.Millisecond):
		log.Println("???")
	}
}

func (c *Client) Do(req *http.Request) (*http.Response, error) {
	host := c.getHost()
	if host == nil {
		return nil, errors.New("no host available")
	}
	u, err := url.Parse(host.Value.(string))
	if err != nil {
		return nil, err
	}
	req.URL.Scheme = u.Scheme
	req.URL.Host = u.Host
	res, err := c.Client.Do(req)
	if err != nil {
		c.moveToPendingHosts(host)
		//retry
		return c.Do(req)
	}
	return res, nil
}

func (c *Client) Get(path string) (*http.Response, error) {
	req, err := http.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func (c *Client) Head(path string) (*http.Response, error) {
	req, err := http.NewRequest("HEAD", path, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func (c *Client) Post(path string, contentType string, body io.Reader) (*http.Response, error) {
	req, err := http.NewRequest("POST", path, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	return c.Do(req)
}

func (c *Client) PostForm(path string, data url.Values) (*http.Response, error) {
	return c.Post(path, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

func (c *Client) CloseIdleConnections() {
	c.Client.CloseIdleConnections()
}
